import tkinter as tk
from tkinter import filedialog, messagebox, Menu, font, colorchooser, simpledialog, ttk
import os
import json
from datetime import datetime
import threading
import atexit
import time
import re
from collections import deque
import shutil
from cryptography.fernet import Fernet
import base64
import hashlib

# 配置文件路径
CONFIG_FILE = "note_app_config.json"
# 默认最大历史步数
MAX_UNDO_STEPS = 100

class NoteApp:
    def __init__(self, root):
        self.root = root
        self.root.title("记事本")
        self.root.geometry("1100x700")
        
        # 当前文件和未保存状态
        self.current_file = None
        self.unsaved_changes = False
        self.auto_save_interval = 300  # 5分钟
        
        # 最近打开文件列表 (可能不再需要，因为我们有 Treeview)
        self.recent_files = []
        
        # 剪贴板历史（最多5次）
        self.clipboard_history = deque(maxlen=5)
        
        # 字体配置
        self.current_font_family = "Consolas"
        self.current_font_size = 12
        self.text_font = font.Font(family=self.current_font_family, size=self.current_font_size)
        
        # --- 新增：本地存储优化属性 ---
        self.notes_root = None # 当前笔记根目录
        self.default_notes_root = None  # 默认笔记根目录
        self.backup_enabled = False
        self.backup_directory = ""
        self.backup_interval = "daily"  # "daily", "weekly", "monthly", "disabled"
        self.backup_keep_count = 7
        # --- 新增结束 ---

        # 加载配置
        self.load_config()
        # 启动时加载或让用户选择根目录
        self.load_notes_root() 

        # 创建文本区域和 Treeview
        self.tree_view = None
        self.text_area = None
        self.create_main_ui()
        
        # 创建菜单栏
        self.create_menu()
        
        # 启动自动保存
        self.start_auto_save()
        # 启动回收站清理
        self.start_trash_cleaner()
        # --- 新增：启动自动备份 ---
        self.start_auto_backup()
        # --- 新增结束 ---
        
        # 注册退出保存
        atexit.register(self.save_config)
        self.root.protocol("WM_DELETE_WINDOW", self.exit_app)
        
        # 撤销/重做栈
        self.undo_stack = deque(maxlen=MAX_UNDO_STEPS)
        self.redo_stack = deque(maxlen=MAX_UNDO_STEPS)
        self.last_save_time = time.time()
        self.save_state()  # 初始状态入栈
        
        # 查找对话框引用
        self.find_window = None
        self.find_query = ""
        self.find_case_sensitive = False
        self.find_matches = []
        self.current_match_index = 0

    def load_notes_root(self):
        """加载或让用户选择笔记根目录"""
        # 优先使用配置文件中的 notes_root，如果没有则使用 default_notes_root
        if not self.notes_root and self.default_notes_root and os.path.isdir(self.default_notes_root):
            self.notes_root = self.default_notes_root
        elif not self.notes_root:
            # 如果都没有有效路径，则让用户选择或使用默认
            root_path = filedialog.askdirectory(title="选择笔记根目录")
            if not root_path:
                # messagebox.showerror("错误", "必须选择一个笔记根目录！")
                # self.root.destroy()
                # return
                # 如果用户取消，创建一个临时目录或提示
                root_path = os.path.join(os.path.expanduser("~"), "MyNotes")
                os.makedirs(root_path, exist_ok=True)
                messagebox.showinfo("信息", f"未选择目录，将使用默认目录: {root_path}")

            self.notes_root = root_path
            # 保存根目录到配置
            self.save_config()
            
        # 确保回收站存在
        os.makedirs(self.get_trash_path(), exist_ok=True)

    def get_trash_path(self):
        """获取回收站路径"""
        return os.path.join(self.notes_root, "__回收站__")

    def create_main_ui(self):
        """创建主UI区域，包含 Treeview 和 文本编辑区"""
        main_paned_window = tk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main_paned_window.pack(fill=tk.BOTH, expand=True)

        # --- 左侧 Treeview 区域 ---
        tree_frame = tk.Frame(main_paned_window, width=250)
        tree_frame.pack(fill=tk.BOTH, expand=True)
        tree_frame.pack_propagate(False) # 保持宽度

        # Treeview 标题
        tree_label = tk.Label(tree_frame, text="笔记结构", anchor='w')
        tree_label.pack(fill=tk.X)

        self.tree_view = ttk.Treeview(tree_frame)
        self.tree_view.heading('#0', text='笔记和文件夹', anchor='w')

        tree_scrollbar_y = tk.Scrollbar(tree_frame, orient="vertical", command=self.tree_view.yview)
        tree_scrollbar_y.pack(side="right", fill="y")
        self.tree_view.configure(yscrollcommand=tree_scrollbar_y.set)
        
        tree_scrollbar_x = tk.Scrollbar(tree_frame, orient="horizontal", command=self.tree_view.xview)
        tree_scrollbar_x.pack(side="bottom", fill="x")
        self.tree_view.configure(xscrollcommand=tree_scrollbar_x.set)

        self.tree_view.pack(fill=tk.BOTH, expand=True)
        main_paned_window.add(tree_frame, minsize=200, sticky='nsew') # 设置最小宽度

        # 绑定 Treeview 事件
        self.tree_view.bind("<<TreeviewSelect>>", self.on_tree_select)
        self.tree_view.bind("<Double-1>", self.on_tree_double_click)
        self.tree_view.bind("<Button-3>", self.on_tree_right_click) # 右键菜单

        # --- 右侧文本编辑区域 ---
        text_frame = tk.Frame(main_paned_window)
        
        # 创建文本区域 (保留原有逻辑，但放入新的框架)
        self.text_area = tk.Text(
            text_frame,
            wrap='word',
            undo=True,
            font=self.text_font,
            spacing1=2,
            spacing2=2,
            spacing3=2
        )
        self.text_area.pack(side='right', expand=True, fill='both')

        # 滚动条
        scrollbar = tk.Scrollbar(self.text_area, command=self.text_area.yview)
        scrollbar.pack(side='right', fill='y')
        self.text_area.config(yscrollcommand=scrollbar.set)

        # 标签配置（用于格式）
        self.setup_text_tags()
        # 监听修改
        self.text_area.bind("<<Modified>>", self.on_text_change)
        
        text_frame.pack(fill=tk.BOTH, expand=True)
        main_paned_window.add(text_frame, sticky='nsew')

        # 加载笔记树
        self.load_notes_tree()

    def load_notes_tree(self):
        """加载笔记文件夹和文件到 Treeview"""
        if not self.notes_root:
            return
        self.tree_view.delete(*self.tree_view.get_children()) # 清空

        # 添加回收站节点
        trash_node = self.tree_view.insert("", "end", "trash_node", text="🗑️ 回收站", values=("trash", self.get_trash_path()), open=False)
        # --- 新增：加载回收站内容 ---
        self.load_trash_contents(trash_node)
        # --------------------------

        # 递归加载笔记目录
        self._load_directory(self.notes_root, "")

    def _load_directory(self, path, parent_node):
        """递归加载目录到 Treeview"""
        try:
            # 获取目录内容并排序（文件夹优先）
            items = sorted(os.listdir(path), key=lambda x: (not os.path.isdir(os.path.join(path, x)), x.lower()))
            for item in items:
                item_path = os.path.join(path, item)
                # rel_path = os.path.relpath(item_path, self.notes_root) # 相对于根目录的路径

                if os.path.isdir(item_path):
                    if item == "__回收站__":
                        # 更新回收站节点的路径（以防 notes_root 改变）
                        self.tree_view.item("trash_node", values=("trash", item_path))
                        continue # 跳过回收站文件夹在主视图的显示
                    node = self.tree_view.insert(parent_node, "end", text=f"📁 {item}", values=("folder", item_path), open=False)
                    self._load_directory(item_path, node) # 递归加载子目录
                elif item.endswith('.txt'):
                    meta_path = item_path + '.meta.json'
                    title = item[:-4] # 默认标题为文件名
                    if os.path.exists(meta_path):
                        try:
                            with open(meta_path, 'r', encoding='utf-8') as f:
                                meta = json.load(f)
                                title = meta.get('title', title)
                        except Exception as e:
                            print(f"读取元数据失败 {meta_path}: {e}")
                    self.tree_view.insert(parent_node, "end", text=f"📄 {title}", values=("note", item_path))
        except Exception as e:
             print(f"加载目录 {path} 出错: {e}")

    def load_trash_contents(self, trash_node_id):
        """加载回收站目录的内容到 Treeview"""
        trash_path = self.get_trash_path()
        if not os.path.exists(trash_path):
            return

        try:
            # 获取回收站内容并排序 (文件夹优先，按名称)
            items = sorted(os.listdir(trash_path), key=lambda x: (not os.path.isdir(os.path.join(trash_path, x)), x.lower()))
            for item in items:
                item_path = os.path.join(trash_path, item)
                
                # 尝试从文件名解析原始名称（简化处理，实际可能需要更复杂的元数据）
                # 假设文件名格式为 timestamp_originalname 或 timestamp_originalname_folder
                original_name = item
                try:
                    # 分割文件名，取时间戳后的部分
                    parts = item.split('_', 3) # 最多分割3次
                    if len(parts) >= 3:
                         # parts[0] = date, parts[1] = time, parts[2] = microseconds_originalname...
                         # 重新组合后面的部分
                         original_name = '_'.join(parts[3:]) if len(parts) > 3 else parts[2]
                         # 如果是文件夹，去掉 "_folder" 后缀
                         if original_name.endswith("_folder"):
                             original_name = original_name[:-7] # Remove "_folder"
                         elif original_name.endswith(".txt.meta.json"):
                             original_name = original_name[:-14] # Remove ".txt.meta.json"
                         elif original_name.endswith(".meta.json"):
                             original_name = original_name[:-10] # Remove ".meta.json"
                         elif original_name.endswith(".txt"):
                             original_name = original_name[:-4] # Remove ".txt"
                except Exception as e:
                    print(f"解析回收站项目名称失败 {item}: {e}")

                if os.path.isdir(item_path):
                    # 检查是否是文件夹 (根据文件名结尾或实际检查)
                    if item.endswith("_folder"):
                        display_name = f"📁 {original_name}"
                        item_type = "folder_trash"
                    else:
                        # 如果在回收站里是个文件夹，但文件名没标记，也按文件夹处理
                        display_name = f"📁 {original_name}"
                        item_type = "folder_trash"
                    self.tree_view.insert(trash_node_id, "end", text=display_name, values=(item_type, item_path), open=False)
                elif item.endswith('.txt'):
                    display_name = f"📄 {original_name}"
                    self.tree_view.insert(trash_node_id, "end", text=display_name, values=("note_trash", item_path))
                # 也处理回收站里的 .meta.json 文件，但不直接显示
                # 可以选择显示所有文件，或者只显示 .txt 和 文件夹
                elif not item.endswith('.meta.json'):
                     # 显示其他非元数据文件
                     display_name = f"📄 {original_name}"
                     self.tree_view.insert(trash_node_id, "end", text=display_name, values=("file_trash", item_path))

        except Exception as e:
             print(f"加载回收站内容 {trash_path} 出错: {e}")

    def on_tree_select(self, event):
        """Treeview 选择事件"""
        # 可以在这里根据选中项类型（文件夹/笔记/回收站）更新 UI 状态
        pass

    def on_tree_double_click(self, event):
        """Treeview 双击事件 - 打开笔记"""
        item_id = self.tree_view.selection()
        if item_id:
            item_type, item_path = self.tree_view.item(item_id, "values")
            if item_type == "note":
                 self.open_note_from_path(item_path)

    def on_tree_right_click(self, event):
        """Treeview 右键事件 - 显示上下文菜单"""
        # 选择被点击的项目
        item_id = self.tree_view.identify_row(event.y)
        if item_id:
            self.tree_view.selection_set(item_id)
        else:
            # 如果点击在空白处，取消选择
            self.tree_view.selection_remove(*self.tree_view.selection())

        # 创建右键菜单
        context_menu = Menu(self.root, tearoff=0)
        
        selected = self.tree_view.selection()
        if selected:
            item_id = selected[0]
            item_type, item_path = self.tree_view.item(item_id, "values")
            
            if item_type == "folder":
                context_menu.add_command(label="新建文件夹", command=lambda: self.create_new_folder_under(item_id))
                context_menu.add_command(label="删除文件夹", command=lambda: self.delete_item(item_id))
            elif item_type == "note":
                context_menu.add_command(label="删除笔记", command=lambda: self.delete_item(item_id))
            # --- 修改：处理回收站内的项目 ---
            elif item_type in ["folder_trash", "note_trash", "file_trash"]:
                # 从回收站恢复需要知道原始路径，这里简化处理，只允许永久删除
                # 或者提供一个“恢复到根目录”选项
                context_menu.add_command(label="恢复到根目录", command=lambda: self.restore_item_from_trash(item_id))
                context_menu.add_command(label="永久删除", command=lambda: self.permanently_delete_item(item_id))
            elif item_type == "trash":
                context_menu.add_command(label="清空回收站", command=self.empty_trash)
            # ---------------------------------
        else:
            # 点击在空白处 (非回收站内)
            context_menu.add_command(label="新建文件夹", command=self.create_new_folder_at_root)
            
        # 在鼠标位置显示菜单
        try:
            context_menu.tk_popup(event.x_root, event.y_root)
        finally:
            context_menu.grab_release()

    def refresh_tree_node(self, folder_path):
        """刷新 Treeview 中的指定文件夹节点 (简化实现：全部刷新)"""
        self.load_notes_tree()

    def create_new_folder_at_root(self):
        """在根目录下创建新文件夹"""
        self.create_new_folder_under("")

    def create_new_folder_under(self, parent_node_id):
        """在选中节点下创建新文件夹"""
        if parent_node_id == "trash_node":
            messagebox.showwarning("警告", "不能在回收站内创建文件夹。")
            return

        parent_path = self.notes_root
        if parent_node_id and parent_node_id != "":
            item_type, item_path = self.tree_view.item(parent_node_id, "values")
            if item_type == "folder":
                parent_path = item_path
            # elif item_type == "note":
            #     parent_path = os.path.dirname(item_path) # 不允许在笔记下建文件夹

        folder_name = simpledialog.askstring("新建文件夹", "请输入文件夹名称:")
        if folder_name:
            # 简单验证文件名
            if any(char in folder_name for char in r'<>:"/\|?*') or folder_name in ['.', '..']:
                 messagebox.showerror("错误", "文件夹名称包含非法字符。")
                 return
            new_folder_path = os.path.join(parent_path, folder_name)
            if os.path.exists(new_folder_path):
                messagebox.showerror("错误", "文件夹已存在。")
                return
            try:
                os.makedirs(new_folder_path, exist_ok=False)
                # 在 Treeview 中添加新节点
                display_name = f"📁 {folder_name}"
                new_node_id = self.tree_view.insert(parent_node_id, "end", text=display_name, values=("folder", new_folder_path), open=False)
                # 展开父节点
                self.tree_view.item(parent_node_id, open=True)
                # 选中新创建的文件夹
                self.tree_view.selection_set(new_node_id)
            except Exception as e:
                messagebox.showerror("错误", f"创建文件夹失败: {e}")

    def delete_item(self, item_id):
        """删除选中的文件夹或笔记到回收站"""
        if not item_id or item_id == "trash_node":
             return

        item_type, item_path = self.tree_view.item(item_id, "values")

        item_name = os.path.basename(item_path)
        display_text = "文件夹" if item_type == "folder" else "笔记"
        if not messagebox.askyesno("确认删除", f"确定要将 '{item_name}' {display_text} 移动到回收站吗?"):
             return

        try:
             trash_path = self.get_trash_path()
             os.makedirs(trash_path, exist_ok=True)
             # 为了避免重名，可以在回收站里加时间戳和原始路径信息
             timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
             # 简化处理：只用时间戳和原名，避免路径过长
             trash_item_name = f"{timestamp}_{item_name}"
             if item_type == "folder":
                 trash_item_name += "_folder" # 区分文件夹
             trash_item_path = os.path.join(trash_path, trash_item_name)

             # 移动文件/文件夹到回收站
             shutil.move(item_path, trash_item_path)
             # 如果是笔记，也移动 .meta.json
             if item_type == "note":
                 meta_path = item_path + '.meta.json'
                 if os.path.exists(meta_path):
                     trash_meta_path = trash_item_path + '.meta.json'
                     shutil.move(meta_path, trash_meta_path)

             # 从 Treeview 移除
             self.tree_view.delete(item_id)
             # 刷新回收站节点内容
             trash_node = self.tree_view.item("trash_node")
             if trash_node:
                 # 清空回收站节点的子项
                 for child in self.tree_view.get_children("trash_node"):
                     self.tree_view.delete(child)
                 # 重新加载回收站内容
                 self.load_trash_contents("trash_node")
        except Exception as e:
             messagebox.showerror("错误", f"删除失败: {e}")

    def empty_trash(self):
        """清空回收站"""
        trash_path = self.get_trash_path()
        if not os.path.exists(trash_path) or not os.listdir(trash_path):
            messagebox.showinfo("回收站", "回收站是空的。")
            return

        if not messagebox.askyesno("清空回收站", "确定要永久删除回收站中的所有项目吗？此操作不可撤销。"):
            return

        try:
            for item in os.listdir(trash_path):
                item_path = os.path.join(trash_path, item)
                if os.path.isfile(item_path):
                    os.remove(item_path)
                elif os.path.isdir(item_path):
                    shutil.rmtree(item_path)
            messagebox.showinfo("回收站", "回收站已清空。")
            # 刷新 Treeview (重新加载回收站内容)
            trash_node = self.tree_view.item("trash_node")
            if trash_node:
                 for child in self.tree_view.get_children("trash_node"):
                     self.tree_view.delete(child)
                 self.load_trash_contents("trash_node")
        except Exception as e:
             messagebox.showerror("错误", f"清空回收站失败: {e}")

    def restore_item_from_trash(self, item_id):
        """从回收站恢复项目到根目录"""
        if not item_id:
             return

        item_type, item_path = self.tree_view.item(item_id, "values")
        
        if item_type not in ["folder_trash", "note_trash", "file_trash"]:
            messagebox.showwarning("警告", "只能恢复回收站中的项目。")
            return

        item_name = os.path.basename(item_path)
        # 尝试解析原始名称
        original_name = item_name
        try:
            parts = item_name.split('_', 3)
            if len(parts) >= 3:
                 original_name = '_'.join(parts[3:]) if len(parts) > 3 else parts[2]
                 if original_name.endswith("_folder"):
                     original_name = original_name[:-7]
                 elif original_name.endswith(".txt.meta.json"):
                     original_name = original_name[:-14]
                 elif original_name.endswith(".meta.json"):
                     original_name = original_name[:-10]
                 elif original_name.endswith(".txt"):
                     original_name = original_name[:-4]
        except:
            pass

        target_path = os.path.join(self.notes_root, original_name)
        
        # 检查目标路径是否已存在
        counter = 1
        base_name, ext = os.path.splitext(original_name)
        final_target_path = target_path
        while os.path.exists(final_target_path) or os.path.exists(final_target_path + '.meta.json'):
             new_name = f"{base_name} (恢复 {counter}){ext}"
             final_target_path = os.path.join(self.notes_root, new_name)
             counter += 1

        if not messagebox.askyesno("确认恢复", f"确定要将 '{original_name}' 恢复到根目录吗?\n文件将被命名为: {os.path.basename(final_target_path)}"):
             return

        try:
             # 移动主文件
             shutil.move(item_path, final_target_path)
             # 如果是笔记，也移动 .meta.json
             if item_type == "note_trash":
                 meta_path = item_path + '.meta.json'
                 if os.path.exists(meta_path):
                     trash_meta_path = final_target_path + '.meta.json'
                     shutil.move(meta_path, trash_meta_path)
             # 从 Treeview 移除
             self.tree_view.delete(item_id)
             # 刷新主目录树 (简单实现)
             self.refresh_tree_node(self.notes_root)
             messagebox.showinfo("恢复成功", f"项目已恢复到: {final_target_path}")
        except Exception as e:
             messagebox.showerror("恢复失败", f"恢复项目失败: {e}")

    def permanently_delete_item(self, item_id):
        """永久删除回收站中的项目"""
        if not item_id:
             return

        item_type, item_path = self.tree_view.item(item_id, "values")
        
        if item_type not in ["folder_trash", "note_trash", "file_trash"]:
            messagebox.showwarning("警告", "只能永久删除回收站中的项目。")
            return

        item_name = os.path.basename(item_path)
         # 尝试解析原始名称用于提示
        original_name = item_name
        try:
            parts = item_name.split('_', 3)
            if len(parts) >= 3:
                 original_name = '_'.join(parts[3:]) if len(parts) > 3 else parts[2]
                 if original_name.endswith("_folder"):
                     original_name = original_name[:-7]
                 elif original_name.endswith(".txt.meta.json"):
                     original_name = original_name[:-14]
                 elif original_name.endswith(".meta.json"):
                     original_name = original_name[:-10]
                 elif original_name.endswith(".txt"):
                     original_name = original_name[:-4]
        except:
            pass

        display_type = "文件夹" if "folder" in item_type else "文件"
        if not messagebox.askyesno("确认永久删除", f"确定要永久删除 '{original_name}' {display_type} 吗？此操作不可撤销。"):
             return

        try:
             # 删除主文件/文件夹
             if os.path.isfile(item_path):
                 os.remove(item_path)
             elif os.path.isdir(item_path):
                 shutil.rmtree(item_path)
             # 如果是笔记，也删除 .meta.json
             if "note" in item_type:
                 meta_path = item_path + '.meta.json'
                 if os.path.exists(meta_path):
                     os.remove(meta_path)
             # 从 Treeview 移除
             self.tree_view.delete(item_id)
             messagebox.showinfo("删除成功", f"项目 '{original_name}' 已被永久删除。")
        except Exception as e:
             messagebox.showerror("删除失败", f"永久删除项目失败: {e}")


    def create_menu(self):
        """创建菜单栏"""
        self.menu_bar = Menu(self.root)
        self.root.config(menu=self.menu_bar)
        # 文件菜单
        file_menu = Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="文件", menu=file_menu)
        new_submenu = Menu(file_menu, tearoff=0)
        new_submenu.add_command(label="空白文档", command=self.new_note)
        new_submenu.add_command(label="待办模板", command=self.new_todo_template)
        new_submenu.add_command(label="日记模板", command=self.new_diary_template)
        file_menu.add_cascade(label="新建", menu=new_submenu)
        file_menu.add_command(label="打开", command=self.open_note, accelerator="Ctrl+O")
        file_menu.add_command(label="保存", command=self.save_note, accelerator="Ctrl+S")
        file_menu.add_command(label="另存为", command=self.save_as_note)
        file_menu.add_command(label="导出为 HTML", command=self.export_as_html)
        # self.recent_menu = Menu(file_menu, tearoff=0)
        # file_menu.add_cascade(label="最近打开", menu=self.recent_menu)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.exit_app)
        # 编辑菜单
        edit_menu = Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="编辑", menu=edit_menu)
        edit_menu.add_command(label="撤销", command=self.undo, accelerator="Ctrl+Z")
        edit_menu.add_command(label="重做", command=self.redo, accelerator="Ctrl+Y")
        edit_menu.add_separator()
        edit_menu.add_command(label="剪切", command=self.cut, accelerator="Ctrl+X")
        edit_menu.add_command(label="复制", command=self.copy, accelerator="Ctrl+C")
        edit_menu.add_command(label="粘贴", command=self.paste, accelerator="Ctrl+V")
        edit_menu.add_command(label="全选", command=self.select_all, accelerator="Ctrl+A")
        edit_menu.add_separator()
        edit_menu.add_command(label="剪贴板历史", command=self.show_clipboard_history)
        # 查找菜单
        search_menu = Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="查找", menu=search_menu)
        search_menu.add_command(label="查找", command=self.open_find_dialog, accelerator="Ctrl+F")
        search_menu.add_command(label="替换", command=self.open_replace_dialog, accelerator="Ctrl+H")
        # 格式菜单
        format_menu = Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="格式", menu=format_menu)
        format_menu.add_command(label="粗体", command=self.toggle_bold)
        format_menu.add_command(label="斜体", command=self.toggle_italic)
        format_menu.add_command(label="下划线", command=self.toggle_underline)
        format_menu.add_command(label="删除线", command=self.toggle_strikethrough)
        format_menu.add_separator()
        format_menu.add_command(label="字体", command=self.change_font)
        format_menu.add_command(label="字号", command=self.change_font_size)
        format_menu.add_command(label="颜色", command=self.change_text_color)
        # 管理菜单
        manage_menu = Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="管理", menu=manage_menu)
        manage_menu.add_command(label="新建文件夹", command=self.create_new_folder_at_root)
        manage_menu.add_command(label="清空回收站", command=self.empty_trash)
        manage_menu.add_separator()
        # manage_menu.add_command(label="选择笔记目录", command=self.select_notes_root) # 可保留或移除
        manage_menu.add_command(label="设置默认存储位置", command=self.set_default_notes_root) # 新增
        manage_menu.add_command(label="设置自动备份", command=self.set_auto_backup) # 新增
        # 绑定快捷键
        self.root.bind("<Control-o>", lambda e: self.open_note())
        self.root.bind("<Control-s>", lambda e: self.save_note())
        self.root.bind("<Control-z>", lambda e: self.undo())
        self.root.bind("<Control-y>", lambda e: self.redo())
        self.root.bind("<Control-x>", lambda e: self.cut())
        self.root.bind("<Control-c>", lambda e: self.copy())
        self.root.bind("<Control-v>", lambda e: self.paste())
        self.root.bind("<Control-a>", lambda e: self.select_all())
        self.root.bind("<Control-f>", lambda e: self.open_find_dialog())
        self.root.bind("<Control-h>", lambda e: self.open_replace_dialog())
        # 更新最近文件菜单
        # self.update_recent_files_menu()

    def set_default_notes_root(self):
        """设置默认笔记根目录"""
        new_default_root = filedialog.askdirectory(title="设置默认笔记存储位置")
        if new_default_root:
            self.default_notes_root = new_default_root
            # 如果当前 notes_root 是默认的或用户确认，也可以一并更改
            # if not self.notes_root or messagebox.askyesno("确认", "是否也将当前笔记目录更改为默认目录？"):
            #     self.notes_root = self.default_notes_root
            #     os.makedirs(self.get_trash_path(), exist_ok=True)
            #     self.load_notes_tree()
            self.save_config()
            messagebox.showinfo("信息", f"默认笔记存储位置已设置为: {new_default_root}")

    def set_auto_backup(self):
        """打开自动备份设置对话框"""
        backup_win = tk.Toplevel(self.root)
        backup_win.title("自动备份设置")
        backup_win.geometry("450x350")
        backup_win.resizable(False, False)
        backup_win.grab_set() # 模态对话框

        # 备份启用
        enable_var = tk.BooleanVar(value=self.backup_enabled)
        enable_check = tk.Checkbutton(backup_win, text="启用自动备份", variable=enable_var)
        enable_check.pack(anchor='w', padx=20, pady=(20, 5))

        # 备份目录
        dir_frame = tk.Frame(backup_win)
        dir_frame.pack(fill='x', padx=20, pady=5)
        tk.Label(dir_frame, text="备份存储位置:").pack(anchor='w')
        dir_var = tk.StringVar(value=self.backup_directory)
        dir_entry = tk.Entry(dir_frame, textvariable=dir_var, state='readonly') # 只读，通过按钮选择
        dir_entry.pack(side='left', fill='x', expand=True, padx=(0, 5))
        def choose_backup_dir():
            chosen_dir = filedialog.askdirectory(title="选择备份存储位置", initialdir=dir_var.get() or self.backup_directory)
            if chosen_dir:
                dir_var.set(chosen_dir)
        tk.Button(dir_frame, text="浏览...", command=choose_backup_dir).pack(side='right')

        # 备份频率
        freq_frame = tk.Frame(backup_win)
        freq_frame.pack(fill='x', padx=20, pady=5)
        tk.Label(freq_frame, text="备份频率:").pack(anchor='w')
        freq_var = tk.StringVar(value=self.backup_interval)
        freq_combo = ttk.Combobox(freq_frame, textvariable=freq_var, state="readonly", 
                                  values=["disabled", "daily", "weekly", "monthly"])
        freq_combo.pack(fill='x')

        # 保留份数
        keep_frame = tk.Frame(backup_win)
        keep_frame.pack(fill='x', padx=20, pady=5)
        tk.Label(keep_frame, text="保留最近备份数量 (0为保留全部):").pack(anchor='w')
        keep_var = tk.StringVar(value=str(self.backup_keep_count))
        keep_entry = tk.Entry(keep_frame, textvariable=keep_var)
        keep_entry.pack(fill='x')

        # 按钮框架
        button_frame = tk.Frame(backup_win)
        button_frame.pack(pady=20)

        def apply_settings():
            self.backup_enabled = enable_var.get()
            self.backup_directory = dir_var.get()
            self.backup_interval = freq_var.get()
            
            try:
                keep_count = int(keep_var.get())
                if keep_count >= 0:
                    self.backup_keep_count = keep_count
                else:
                    raise ValueError
            except ValueError:
                messagebox.showwarning("输入错误", "保留份数必须是非负整数。")
                return

            self.save_config()
            # 重启备份任务可能比较复杂，简单提示用户重启程序以应用新设置
            # 或者在这里尝试停止旧线程（如果可实现）并启动新线程
            messagebox.showinfo("设置成功", "自动备份设置已保存。")
            backup_win.destroy()

        tk.Button(button_frame, text="确定", command=apply_settings).pack(side='left', padx=10)
        tk.Button(button_frame, text="取消", command=backup_win.destroy).pack(side='left', padx=10)
        # 添加一个“立即备份”测试按钮（可选）
        tk.Button(button_frame, text="立即备份", command=lambda: self.perform_backup(force=True)).pack(side='left', padx=10)


    def get_backup_interval_seconds(self):
        """根据备份频率获取间隔秒数"""
        intervals = {
            "daily": 24 * 60 * 60,
            "weekly": 7 * 24 * 60 * 60,
            "monthly": 30 * 24 * 60 * 60 # 简化处理，实际每月天数不同
        }
        return intervals.get(self.backup_interval, 24 * 60 * 60) # 默认每天

    def start_auto_backup(self):
        """启动后台自动备份线程"""
        def loop_with_interval():
             while True:
                 if self.backup_enabled and self.backup_directory and self.notes_root:
                     try:
                         self.perform_backup()
                     except Exception as e:
                         print(f"自动备份出错: {e}")
                     # 清理旧备份
                     try:
                         self.clean_old_backups()
                     except Exception as e:
                         print(f"清理旧备份出错: {e}")
                         
                 # 计算下次备份时间间隔
                 interval = self.get_backup_interval_seconds()
                 if interval <= 0 or not self.backup_enabled:
                     interval = 60 # 如果禁用或无效，短间隔检查
                 time.sleep(interval)
                 # time.sleep(30) # 测试用 30 秒

        # 使用基于间隔的线程
        t = threading.Thread(target=loop_with_interval, daemon=True)
        t.start()

    def perform_backup(self, force=False):
        """执行一次备份"""
        if not self.backup_enabled and not force:
            return
        if not self.notes_root or not self.backup_directory:
            print("备份路径未设置")
            return

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_name = f"notes_backup_{timestamp}"
        backup_path = os.path.join(self.backup_directory, backup_name)

        # 检查源目录和目标目录
        if not os.path.exists(self.notes_root):
            print(f"源目录不存在: {self.notes_root}")
            return
        if not os.path.exists(self.backup_directory):
            try:
                os.makedirs(self.backup_directory, exist_ok=True)
            except Exception as e:
                print(f"无法创建备份目录: {e}")
                return

        # 检查目标是否已存在
        if os.path.exists(backup_path):
            print(f"备份目标已存在: {backup_path}")
            return # 或者添加计数器避免冲突

        try:
            # 使用 shutil.copytree 进行完整目录复制
            # 注意：copytree 要求目标目录不存在
            shutil.copytree(self.notes_root, backup_path)
            print(f"备份成功: {backup_path}")
            # 可以在这里触发 UI 更新提示，但后台线程操作 UI 需要 after
            # self.root.after(0, lambda: messagebox.showinfo("备份", f"备份完成: {backup_path}")) # 可能导致弹窗过多
        except Exception as e:
            error_msg = f"备份失败: {e}"
            print(error_msg)
            # self.root.after(0, lambda: messagebox.showerror("备份失败", error_msg))

    def clean_old_backups(self):
        """清理旧的备份文件"""
        if not self.backup_directory or self.backup_keep_count <= 0:
            return

        try:
            backups = []
            for item in os.listdir(self.backup_directory):
                item_path = os.path.join(self.backup_directory, item)
                # 假设备份文件夹名以 notes_backup_ 开头
                if os.path.isdir(item_path) and item.startswith("notes_backup_"):
                    # 尝试解析时间戳
                    try:
                        # timestamp_str = item[len("notes_backup_"):]
                        # backup_time = datetime.strptime(timestamp_str, "%Y%m%d_%H%M%S")
                        # 更健壮的解析
                        parts = item.split('_')
                        if len(parts) >= 3 and parts[0] == "notes" and parts[1] == "backup":
                             timestamp_str = f"{parts[2]}_{parts[3]}"
                             backup_time = datetime.strptime(timestamp_str, "%Y%m%d_%H%M%S")
                             backups.append((backup_time, item_path))
                    except ValueError:
                        # 如果无法解析时间戳，跳过
                        print(f"无法解析备份时间戳: {item}")
                        pass

            # 按时间排序，旧的在前
            backups.sort(key=lambda x: x[0])

            # 计算需要删除的数量
            num_to_delete = len(backups) - self.backup_keep_count
            if num_to_delete > 0:
                for i in range(num_to_delete):
                    backup_time, backup_path = backups[i]
                    try:
                        shutil.rmtree(backup_path)
                        print(f"已删除旧备份: {backup_path}")
                    except Exception as e:
                        print(f"删除旧备份失败 {backup_path}: {e}")

        except Exception as e:
            print(f"清理旧备份时出错: {e}")

    def setup_text_tags(self):
        """配置文本样式标签"""
        # 粗体
        self.text_area.tag_configure("bold", font=font.Font(**self.get_font_config(weight="bold")))
        # 斜体
        self.text_area.tag_configure("italic", font=font.Font(**self.get_font_config(slant="italic")))
        # 下划线
        self.text_area.tag_configure("underline", underline=True)
        # 删除线
        self.text_area.tag_configure("strikethrough", overstrike=True)
        # 查找高亮
        self.text_area.tag_configure("find_highlight", background="yellow")

    def get_font_config(self, weight=None, slant=None):
        """获取当前字体配置，可叠加粗体/斜体"""
        return {
            "family": self.current_font_family,
            "size": self.current_font_size,
            "weight": weight or "normal",
            "slant": slant or "roman"
        }

    def on_text_change(self, event=None):
        """文本修改监听"""
        if self.text_area.edit_modified():
            self.unsaved_changes = True
            self.update_title()
            # 防抖保存状态
            current_time = time.time()
            if current_time - self.last_save_time > 0.5:  # 500ms防抖
                self.save_state()
                self.last_save_time = current_time
            self.text_area.edit_modified(False)

    def update_title(self, custom_title=None):
        """更新窗口标题"""
        if custom_title:
            filename = custom_title
        else:
            filename = os.path.basename(self.current_file) if self.current_file else "无标题"
        title = f"记事本 - {filename}"
        if self.unsaved_changes:
            title += "*"
        self.root.title(title)

    def confirm_save_if_needed(self):
        """确认是否保存未保存更改"""
        if not self.unsaved_changes:
            return True
        response = messagebox.askyesnocancel("未保存的更改", "是否保存当前笔记？")
        if response is True:
            return self.save_note()
        elif response is False:
            return True
        return False

    # ==================== 新建功能 ====================
    def get_target_folder_for_new_note(self):
        """确定新建笔记的目标文件夹"""
        target_folder = self.notes_root
        selected = self.tree_view.selection()
        if selected:
            item_type, item_path = self.tree_view.item(selected[0], "values")
            if item_type == "folder":
                target_folder = item_path
            elif item_type == "note":
                target_folder = os.path.dirname(item_path)
            # 回收站内不能新建
            elif item_type == "trash":
                target_folder = self.notes_root
        return target_folder

    def new_note(self):
        if not self.confirm_save_if_needed():
            return
        self.text_area.delete(1.0, tk.END)
        self.current_file = None
        self.unsaved_changes = False
        self.update_title()
        self.reset_format_tags()

    def new_todo_template(self):
        if not self.confirm_save_if_needed():
            return
        self.text_area.delete(1.0, tk.END)
        todo_template = """# 待办事项清单
- [ ] 完成任务 A
- [ ] 完成任务 B
- [ ] 完成任务 C
"""
        self.text_area.insert(tk.END, todo_template)
        self.current_file = None
        self.unsaved_changes = True
        self.update_title()

    def new_diary_template(self):
        if not self.confirm_save_if_needed():
            return
        self.text_area.delete(1.0, tk.END)
        today = datetime.today().strftime('%Y-%m-%d')
        week_day = datetime.today().strftime('%A')
        diary_template = f"""# 日记 - {today} ({week_day})
## 今日所做：
- 
## 遇到的问题：
- 
## 感悟与反思：
- 
"""
        self.text_area.insert(tk.END, diary_template)
        self.current_file = None
        self.unsaved_changes = True
        self.update_title()

    # ==================== 文件操作 ====================
    def open_note(self):
        # 通过 Treeview 打开更直观，此函数可以保留用于打开任意文件
        if not self.confirm_save_if_needed():
            return
        file_path = filedialog.askopenfilename(
            initialdir=self.notes_root,
            defaultextension=".txt",
            filetypes=[("文本文件", "*.txt"), ("Markdown 文件", "*.md"), ("HTML 文件", "*.html"), ("所有文件", "*.*")]
        )
        if not file_path:
            return
        try:
            with open(file_path, 'r', encoding='utf-8') as file:
                content = file.read()
                self.text_area.delete(1.0, tk.END)
                self.text_area.insert(tk.END, content)
                self.current_file = file_path
                self.unsaved_changes = False
                # 从元数据读取标题
                title = os.path.splitext(os.path.basename(file_path))[0]
                meta_path = file_path + '.meta.json'
                if os.path.exists(meta_path):
                    try:
                        with open(meta_path, 'r', encoding='utf-8') as f:
                             meta = json.load(f)
                             title = meta.get('title', title)
                    except:
                        pass
                self.update_title(title)
                # self.add_to_recent_files(file_path) # Treeview 下可能不需要
                self.reset_format_tags()
        except Exception as e:
            messagebox.showerror("打开失败", f"无法打开文件：{str(e)}")

    def open_note_from_path(self, file_path):
        """根据路径打开笔记 (供 Treeview 调用)"""
        if not self.confirm_save_if_needed():
            return
        try:
            with open(file_path, 'r', encoding='utf-8') as file:
                content = file.read()
                self.text_area.delete(1.0, tk.END)
                self.text_area.insert(tk.END, content)
                self.current_file = file_path
                self.unsaved_changes = False
                # 从元数据读取标题
                title = os.path.splitext(os.path.basename(file_path))[0]
                meta_path = file_path + '.meta.json'
                if os.path.exists(meta_path):
                    try:
                        with open(meta_path, 'r', encoding='utf-8') as f:
                             meta = json.load(f)
                             title = meta.get('title', title)
                    except:
                        pass
                # self.add_to_recent_files(file_path) # 可选
                self.update_title(title) # 修改 update_title 以接受自定义标题
                self.reset_format_tags()
        except Exception as e:
            messagebox.showerror("打开失败", f"无法打开文件：{str(e)}")

    def save_note(self):
        if self.current_file:
            try:
                with open(self.current_file, 'w', encoding='utf-8') as file:
                    file.write(self.text_area.get(1.0, tk.END).strip())
                # 更新元数据中的修改时间
                meta_path = self.current_file + '.meta.json'
                meta = {}
                if os.path.exists(meta_path):
                    try:
                        with open(meta_path, 'r', encoding='utf-8') as f:
                             meta = json.load(f)
                    except:
                        pass
                meta['modified_time'] = datetime.now().isoformat()
                with open(meta_path, 'w', encoding='utf-8') as f:
                     json.dump(meta, f, ensure_ascii=False, indent=2)

                self.unsaved_changes = False
                self.update_title() # 如果标题来自元数据，可能需要更新
                # self.refresh_tree_node(os.path.dirname(self.current_file)) # 刷新 Treeview 以反映可能的标题变化 (简单刷新)
                return True
            except Exception as e:
                messagebox.showerror("保存失败", f"无法保存文件：{str(e)}")
                return False
        else:
            return self.save_as_note()

    def save_as_note(self):
        target_folder = self.get_target_folder_for_new_note()
        
        # 生成默认文件名
        base_name = "未命名笔记"
        counter = 1
        file_path = os.path.join(target_folder, f"{base_name}.txt")
        while os.path.exists(file_path):
             file_path = os.path.join(target_folder, f"{base_name} ({counter}).txt")
             counter += 1

        # file_path = filedialog.asksaveasfilename(
        #     initialdir=target_folder,
        #     defaultextension=".txt",
        #     filetypes=[("文本文件", "*.txt"), ("Markdown 文件", "*.md"), ("HTML 文件", "*.html"), ("所有文件", "*.*")]
        # )
        # if not file_path or not file_path.startswith(self.notes_root): # 确保在笔记根目录内
        #     return False
        
        # ... 现有保存 .txt 文件逻辑 ...
        try:
            with open(file_path, 'w', encoding='utf-8') as file:
                file.write(self.text_area.get(1.0, tk.END).strip())
            # 创建/更新元数据
            meta_path = file_path + '.meta.json'
            meta = {
                'title': os.path.splitext(os.path.basename(file_path))[0],
                'created_time': datetime.now().isoformat(),
                'modified_time': datetime.now().isoformat(),
                # 'custom_order': 0 # 如果需要自定义排序
            }
            with open(meta_path, 'w', encoding='utf-8') as f:
                 json.dump(meta, f, ensure_ascii=False, indent=2)

            self.current_file = file_path
            self.unsaved_changes = False
            self.update_title()
            # self.add_to_recent_files(file_path) # 可选：是否保留最近文件列表
            self.refresh_tree_node(os.path.dirname(file_path)) # 刷新 Treeview
            # 在 Treeview 中选中新创建的笔记
            # 这需要遍历 Treeview 找到对应的节点并选中，这里简化处理
            return True
        except Exception as e:
            messagebox.showerror("保存失败", f"无法保存文件：{str(e)}")
            return False

    def export_as_html(self):
        file_path = filedialog.asksaveasfilename(
            defaultextension=".html",
            filetypes=[("HTML 文件", "*.html"), ("所有文件", "*.*")]
        )
        if not file_path:
            return
        try:
            content = self.text_area.get(1.0, tk.END).strip()
            html_content = self.advanced_md_to_html(content)
            with open(file_path, 'w', encoding='utf-8') as file:
                file.write(html_content)
            messagebox.showinfo("导出成功", f"已导出为HTML：\n{file_path}")
        except Exception as e:
            messagebox.showerror("导出失败", f"导出失败：{str(e)}")

    def advanced_md_to_html(self, md_text):
        lines = md_text.splitlines()
        html = ["<!DOCTYPE html>", "<html><head><meta charset='utf-8'>",
                "<style>body { font-family: Arial, sans-serif; margin: 40px; }</style>",
                "</head><body>"]
        in_list = False
        in_code_block = False
        for line in lines:
            line = line.strip()
            # 代码块处理
            if line.startswith("```"):
                if not in_code_block:
                    html.append("<pre><code>")
                    in_code_block = True
                else:
                    html.append("</code></pre>")
                    in_code_block = False
                continue
            if in_code_block:
                html.append(line)
                continue
            if not line:
                if in_list:
                    html.append("</ul>")
                    in_list = False
                html.append("<br>")
                continue
            # 标题处理
            if line.startswith("# "):
                html.append(f"<h1>{self.process_inline_formatting(line[2:])}</h1>")
            elif line.startswith("## "):
                html.append(f"<h2>{self.process_inline_formatting(line[3:])}</h2>")
            elif line.startswith("### "):
                html.append(f"<h3>{self.process_inline_formatting(line[4:])}</h3>")
            # 列表处理
            elif line.startswith("- "):
                if not in_list:
                    html.append("<ul>")
                    in_list = True
                html.append(f"<li>{self.process_inline_formatting(line[2:])}</li>")
            else:
                if in_list:
                    html.append("</ul>")
                    in_list = False
                html.append(f"<p>{self.process_inline_formatting(line)}</p>")
        if in_list:
            html.append("</ul>")
        html.append("</body></html>")
        return "\n".join(html)

    def process_inline_formatting(self, text):
        """处理行内格式（粗体、斜体等）"""
        # 粗体 **text**
        text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text)
        # 斜体 *text*
        text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text)
        # 代码 `code`
        text = re.sub(r'`(.*?)`', r'<code>\1</code>', text)
        return text

    # ==================== 最近文件 ====================
    # (Treeview 模式下可能不再需要)
    # def add_to_recent_files(self, file_path):
    #     if file_path in self.recent_files:
    #         self.recent_files.remove(file_path)
    #     self.recent_files.insert(0, file_path)
    #     self.recent_files = self.recent_files[:10]
    #     self.update_recent_files_menu()
    #     self.save_config()
    # def update_recent_files_menu(self):
    #     self.recent_menu.delete(0, tk.END)
    #     if not self.recent_files:
    #         self.recent_menu.add_command(label="无最近文件", state="disabled")
    #     else:
    #         for fp in self.recent_files:
    #             name = os.path.basename(fp)
    #             self.recent_menu.add_command(label=name, command=lambda f=fp: self.open_recent_file(f))
    # def open_recent_file(self, file_path):
    #     if not os.path.exists(file_path):
    #         messagebox.showwarning("文件不存在", f"文件已删除或移动：\n{file_path}")
    #         self.recent_files.remove(file_path)
    #         self.update_recent_files_menu()
    #         self.save_config()
    #         return
    #     if not self.confirm_save_if_needed():
    #         return
    #     try:
    #         with open(file_path, 'r', encoding='utf-8') as file:
    #             content = file.read()
    #             self.text_area.delete(1.0, tk.END)
    #             self.text_area.insert(tk.END, content)
    #             self.current_file = file_path
    #             self.unsaved_changes = False
    #             self.update_title()
    #             self.add_to_recent_files(file_path)
    #             self.reset_format_tags()
    #     except Exception as e:
    #         messagebox.showerror("打开失败", f"无法打开：{str(e)}")

    # ==================== 撤销/重做 ====================
    def save_state(self):
        """保存当前文本状态到撤销栈"""
        content = self.text_area.get(1.0, tk.END)[:-1]  # 去掉末尾换行
        sel = self.text_area.tag_ranges(tk.SEL)
        selection = (sel[0].string, sel[1].string) if sel else None
        # 保存格式标签信息
        tags_info = {}
        for tag in ["bold", "italic", "underline", "strikethrough"]:
            ranges = self.text_area.tag_ranges(tag)
            if ranges:
                tags_info[tag] = [(ranges[i].string, ranges[i+1].string) for i in range(0, len(ranges), 2)]
        self.undo_stack.append({
            "content": content,
            "selection": selection,
            "font_family": self.current_font_family,
            "font_size": self.current_font_size,
            "tags": tags_info
        })
        self.redo_stack.clear()  # 清空重做栈

    def undo(self):
        if not self.undo_stack:
            return
        state = self.undo_stack.pop()
        self.redo_stack.append({
            "content": self.text_area.get(1.0, tk.END)[:-1],
            "selection": self.get_selection(),
            "font_family": self.current_font_family,
            "font_size": self.current_font_size,
            "tags": self.get_tags_info()
        })
        self.restore_state(state)

    def redo(self):
        if not self.redo_stack:
            return
        state = self.redo_stack.pop()
        self.undo_stack.append({
            "content": self.text_area.get(1.0, tk.END)[:-1],
            "selection": self.get_selection(),
            "font_family": self.current_font_family,
            "font_size": self.current_font_size,
            "tags": self.get_tags_info()
        })
        self.restore_state(state)

    def restore_state(self, state):
        self.text_area.delete(1.0, tk.END)
        self.text_area.insert(1.0, state["content"])
        # 恢复选中状态
        if state["selection"]:
            try:
                self.text_area.tag_add(tk.SEL, state["selection"][0], state["selection"][1])
                self.text_area.mark_set(tk.INSERT, state["selection"][0])
                self.text_area.see(tk.INSERT)
            except tk.TclError:
                pass
        # 恢复字体
        self.current_font_family = state["font_family"]
        self.current_font_size = state["font_size"]
        self.text_font.config(family=self.current_font_family, size=self.current_font_size)
        # 恢复格式标签
        self.reset_format_tags()
        if "tags" in state:
            for tag, ranges in state["tags"].items():
                for start, end in ranges:
                    self.text_area.tag_add(tag, start, end)
        self.unsaved_changes = True
        self.update_title()

    def get_selection(self):
        sel = self.text_area.tag_ranges(tk.SEL)
        return (sel[0].string, sel[1].string) if sel else None

    def get_tags_info(self):
        """获取当前格式标签信息"""
        tags_info = {}
        for tag in ["bold", "italic", "underline", "strikethrough"]:
            ranges = self.text_area.tag_ranges(tag)
            if ranges:
                tags_info[tag] = [(ranges[i].string, ranges[i+1].string) for i in range(0, len(ranges), 2)]
        return tags_info

    # ==================== 剪贴板操作 ====================
    def cut(self):
        try:
            self.copy()
            self.text_area.delete(tk.SEL_FIRST, tk.SEL_LAST)
        except tk.TclError:
            pass

    def copy(self):
        try:
            content = self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST)
            self.root.clipboard_clear()
            self.root.clipboard_append(content)
            # 添加到历史
            if content.strip() and (not self.clipboard_history or self.clipboard_history[0] != content):
                self.clipboard_history.appendleft(content)
        except tk.TclError:
            pass

    def paste(self):
        try:
            content = self.root.clipboard_get()
            self.text_area.insert(tk.INSERT, content)
        except tk.TclError:
            messagebox.showinfo("提示", "剪贴板为空")

    def show_clipboard_history(self):
        win = tk.Toplevel(self.root)
        win.title("剪贴板历史")
        win.geometry("400x300")
        listbox = tk.Listbox(win)
        listbox.pack(expand=True, fill='both', padx=10, pady=10)
        for i, text in enumerate(self.clipboard_history):
            listbox.insert(tk.END, f"{i+1}. {text[:30]}{'...' if len(text) > 30 else ''}")
        def insert_selected():
            sel = listbox.curselection()
            if sel:
                self.text_area.insert(tk.INSERT, self.clipboard_history[sel[0]])
        tk.Button(win, text="插入选中内容", command=insert_selected).pack(pady=5)
        tk.Button(win, text="关闭", command=win.destroy).pack(pady=5)

    # ==================== 文本选择 ====================
    def select_all(self, event=None):
        self.text_area.tag_add(tk.SEL, "1.0", tk.END)
        self.text_area.mark_set(tk.INSERT, "1.0")
        self.text_area.see(tk.INSERT)

    # ==================== 查找与替换 ====================
    def open_find_dialog(self):
        if self.find_window and self.find_window.winfo_exists():
            self.find_window.lift()
            return
        self.find_window = tk.Toplevel(self.root)
        self.find_window.title("查找")
        self.find_window.geometry("400x200")
        self.find_window.resizable(False, False)
        # 查找内容
        tk.Label(self.find_window, text="查找内容：").pack(pady=5)
        self.find_query_var = tk.StringVar()
        query_entry = tk.Entry(self.find_window, textvariable=self.find_query_var, width=40)
        query_entry.pack(pady=5)
        query_entry.focus()
        # 选项
        self.find_case_var = tk.BooleanVar()
        tk.Checkbutton(self.find_window, text="匹配大小写", variable=self.find_case_var).pack()
        # 按钮框架
        button_frame = tk.Frame(self.find_window)
        button_frame.pack(pady=10)
        tk.Button(button_frame, text="查找下一个", command=self.find_next).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="查找上一个", command=self.find_previous).pack(side=tk.LEFT, padx=5)
        tk.Button(self.find_window, text="关闭", command=self.close_find_dialog).pack(pady=5)
        # 绑定回车键
        query_entry.bind("<Return>", lambda e: self.find_next())
        # 绑定窗口关闭事件
        self.find_window.protocol("WM_DELETE_WINDOW", self.close_find_dialog)

    def close_find_dialog(self):
        """关闭查找对话框时清除高亮"""
        self.clear_highlights()
        if self.find_window:
            self.find_window.destroy()
            self.find_window = None

    def find_next(self):
        query = self.find_query_var.get()
        if not query:
            return
        # 更新查找参数
        self.find_query = query
        self.find_case_sensitive = self.find_case_var.get()
        # 清除之前的高亮
        self.clear_highlights()
        start_pos = self.text_area.index(tk.INSERT)
        pos = self.text_area.search(query, start_pos, nocase=not self.find_case_sensitive, stopindex=tk.END)
        # 如果没找到，从开头继续查找
        if not pos:
            pos = self.text_area.search(query, "1.0", nocase=not self.find_case_sensitive, stopindex=start_pos)
        if pos:
            end_pos = f"{pos}+{len(query)}c"
            self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
            self.text_area.tag_add(tk.SEL, pos, end_pos)
            self.text_area.tag_add("find_highlight", pos, end_pos)  # 高亮显示
            self.text_area.mark_set(tk.INSERT, end_pos)
            self.text_area.see(pos)
        else:
            messagebox.showinfo("查找", "未找到匹配内容")

    def find_previous(self):
        query = self.find_query_var.get()
        if not query:
            return
        # 更新查找参数
        self.find_query = query
        self.find_case_sensitive = self.find_case_var.get()
        # 清除之前的高亮
        self.clear_highlights()
        start_pos = self.text_area.index(tk.INSERT)
        pos = self.text_area.search(query, start_pos, nocase=not self.find_case_sensitive, backwards=True)
        # 如果没找到，从末尾继续查找
        if not pos:
            pos = self.text_area.search(query, tk.END, nocase=not self.find_case_sensitive, backwards=True, stopindex=start_pos)
        if pos:
            end_pos = f"{pos}+{len(query)}c"
            self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
            self.text_area.tag_add(tk.SEL, pos, end_pos)
            self.text_area.tag_add("find_highlight", pos, end_pos)  # 高亮显示
            self.text_area.mark_set(tk.INSERT, pos)
            self.text_area.see(pos)
        else:
            messagebox.showinfo("查找", "未找到匹配内容")

    def clear_highlights(self):
        self.text_area.tag_remove("find_highlight", "1.0", tk.END)

    def open_replace_dialog(self):
        win = tk.Toplevel(self.root)
        win.title("替换")
        win.geometry("400x250")
        win.resizable(False, False)
        # 查找内容
        tk.Label(win, text="查找内容：").pack(pady=5)
        find_var = tk.StringVar()
        find_entry = tk.Entry(win, textvariable=find_var, width=40)
        find_entry.pack(pady=5)
        find_entry.focus()
        # 替换为
        tk.Label(win, text="替换为：").pack(pady=5)
        replace_var = tk.StringVar()
        tk.Entry(win, textvariable=replace_var, width=40).pack(pady=5)
        # 选项
        case_var = tk.BooleanVar()
        tk.Checkbutton(win, text="匹配大小写", variable=case_var).pack()
        # 按钮框架
        button_frame = tk.Frame(win)
        button_frame.pack(pady=10)
        def replace():
            query = find_var.get()
            if not query:
                return
            try:
                # 检查当前是否有选中内容，并且选中的内容是否匹配查找内容
                start = self.text_area.index(tk.SEL_FIRST)
                end = self.text_area.index(tk.SEL_LAST)
                selected_text = self.text_area.get(start, end)
                # 检查选中内容是否匹配查找内容
                match = False
                if case_var.get():
                    match = (selected_text == query)
                else:
                    match = (selected_text.lower() == query.lower())
                if match:
                    # 替换选中的内容
                    self.text_area.delete(start, end)
                    self.text_area.insert(start, replace_var.get())
                    # 移动光标到替换后的位置
                    new_end = f"{start}+{len(replace_var.get())}c"
                    self.text_area.mark_set(tk.INSERT, new_end)
                    self.text_area.see(new_end)
                else:
                    # 如果当前选中内容不匹配，查找下一个匹配项
                    self.find_next_for_replace(query, case_var.get(), replace_var.get())
            except tk.TclError:
                # 没有选中内容，查找并替换第一个匹配项
                self.find_and_replace_first(query, case_var.get(), replace_var.get())
        def replace_all():
            query = find_var.get()
            if not query:
                return
            content = self.text_area.get("1.0", tk.END)
            if case_var.get():
                new_content = content.replace(query, replace_var.get())
            else:
                # 不区分大小写的替换
                pattern = re.compile(re.escape(query), re.IGNORECASE)
                new_content = pattern.sub(replace_var.get(), content)
            if new_content != content:
                self.text_area.delete("1.0", tk.END)
                self.text_area.insert("1.0", new_content)
                messagebox.showinfo("替换", f"已完成替换")
        tk.Button(button_frame, text="替换", command=replace).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="全部替换", command=replace_all).pack(side=tk.LEFT, padx=5)
        tk.Button(win, text="关闭", command=win.destroy).pack(pady=5)
        # 绑定回车键到替换功能
        find_entry.bind("<Return>", lambda e: replace())

    def find_next_for_replace(self, query, case_sensitive, replace_text):
        """查找下一个匹配项并替换"""
        start_pos = self.text_area.index(tk.INSERT)
        pos = self.text_area.search(query, start_pos, nocase=not case_sensitive, stopindex=tk.END)
        # 如果没找到，从开头继续查找
        if not pos:
            pos = self.text_area.search(query, "1.0", nocase=not case_sensitive, stopindex=start_pos)
        if pos:
            end_pos = f"{pos}+{len(query)}c"
            # 替换内容
            self.text_area.delete(pos, end_pos)
            self.text_area.insert(pos, replace_text)
            # 选中替换后的内容
            new_end = f"{pos}+{len(replace_text)}c"
            self.text_area.tag_add(tk.SEL, pos, new_end)
            self.text_area.mark_set(tk.INSERT, new_end)
            self.text_area.see(pos)
        else:
            messagebox.showinfo("替换", "未找到匹配内容")

    def find_and_replace_first(self, query, case_sensitive, replace_text):
        """查找并替换第一个匹配项"""
        pos = self.text_area.search(query, "1.0", nocase=not case_sensitive, stopindex=tk.END)
        if pos:
            end_pos = f"{pos}+{len(query)}c"
            # 替换内容
            self.text_area.delete(pos, end_pos)
            self.text_area.insert(pos, replace_text)
            # 选中替换后的内容
            new_end = f"{pos}+{len(replace_text)}c"
            self.text_area.tag_add(tk.SEL, pos, new_end)
            self.text_area.mark_set(tk.INSERT, new_end)
            self.text_area.see(pos)
        else:
            messagebox.showinfo("替换", "未找到匹配内容")

    # ==================== 格式调整 ====================
    def reset_format_tags(self):
        """清除所有格式标签"""
        for tag in ["bold", "italic", "underline", "strikethrough", "find_highlight"]:
            self.text_area.tag_remove(tag, "1.0", tk.END)

    def toggle_tag(self, tag_name):
        """切换指定标签（加/去）"""
        try:
            start, end = self.text_area.index(tk.SEL_FIRST), self.text_area.index(tk.SEL_LAST)
        except tk.TclError:
            return  # 无选中
        # 检查是否已有该标签
        if self.text_area.tag_nextrange(tag_name, start, end):
            self.text_area.tag_remove(tag_name, start, end)
        else:
            self.text_area.tag_add(tag_name, start, end)

    def toggle_bold(self):
        self.toggle_tag("bold")

    def toggle_italic(self):
        self.toggle_tag("italic")

    def toggle_underline(self):
        self.toggle_tag("underline")

    def toggle_strikethrough(self):
        self.toggle_tag("strikethrough")

    def change_font(self):
        """字体选择对话框"""
        # 创建字体选择对话框
        font_window = tk.Toplevel(self.root)
        font_window.title("选择字体")
        font_window.geometry("400x500")
        font_window.resizable(False, False)
        # 获取可用字体列表
        available_fonts = sorted(font.families())
        # 创建列表框
        tk.Label(font_window, text="选择字体：").pack(pady=5)
        font_listbox = tk.Listbox(font_window, width=50, height=20)
        font_listbox.pack(pady=5, padx=10, fill='both', expand=True)
        # 添加字体到列表框
        for f in available_fonts:
            font_listbox.insert(tk.END, f)
        # 滚动条
        scrollbar = tk.Scrollbar(font_listbox)
        scrollbar.pack(side='right', fill='y')
        font_listbox.config(yscrollcommand=scrollbar.set)
        scrollbar.config(command=font_listbox.yview)
        # 当前选中的字体
        selected_font_var = tk.StringVar(value=self.current_font_family)
        def on_font_select(event=None):
            selection = font_listbox.curselection()
            if selection:
                selected_font = font_listbox.get(selection[0])
                selected_font_var.set(selected_font)
        font_listbox.bind('<<ListboxSelect>>', on_font_select)
        # 预选当前字体
        try:
            current_index = available_fonts.index(self.current_font_family)
            font_listbox.selection_set(current_index)
            font_listbox.see(current_index)
        except ValueError:
            pass
        def apply_font():
            selected_font = selected_font_var.get()
            if selected_font and selected_font in available_fonts:
                self.current_font_family = selected_font
                # 只对选中内容应用字体，不改变全局字体
                self.apply_font_to_selection()
                font_window.destroy()
            else:
                messagebox.showwarning("警告", "请选择有效的字体")
        # 按钮框架
        button_frame = tk.Frame(font_window)
        button_frame.pack(pady=10)
        tk.Button(button_frame, text="确定", command=apply_font).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="取消", command=font_window.destroy).pack(side=tk.LEFT, padx=5)

    def change_font_size(self):
        """字号选择对话框"""
        # 创建字号选择对话框
        size_window = tk.Toplevel(self.root)
        size_window.title("选择字号")
        size_window.geometry("300x400")
        size_window.resizable(False, False)
        # 常用字号列表
        common_sizes = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
        # 创建列表框
        tk.Label(size_window, text="选择字号：").pack(pady=5)
        size_listbox = tk.Listbox(size_window, width=30, height=15)
        size_listbox.pack(pady=5, padx=10, fill='both', expand=True)
        # 添加字号到列表框
        for size in common_sizes:
            size_listbox.insert(tk.END, str(size))
        # 当前选中的字号
        selected_size_var = tk.StringVar(value=str(self.current_font_size))
        def on_size_select(event=None):
            selection = size_listbox.curselection()
            if selection:
                selected_size = size_listbox.get(selection[0])
                selected_size_var.set(selected_size)
        size_listbox.bind('<<ListboxSelect>>', on_size_select)
        # 预选当前字号
        try:
            current_index = common_sizes.index(self.current_font_size)
            size_listbox.selection_set(current_index)
            size_listbox.see(current_index)
        except ValueError:
            pass
        def apply_size():
            try:
                selected_size = int(selected_size_var.get())
                if 6 <= selected_size <= 128:
                    self.current_font_size = selected_size
                    # 只对选中内容应用字号，不改变全局字号
                    self.apply_font_to_selection()
                    size_window.destroy()
                else:
                    messagebox.showwarning("警告", "字号必须在6-128之间")
            except ValueError:
                messagebox.showwarning("警告", "请输入有效的数字")
        # 按钮框架
        button_frame = tk.Frame(size_window)
        button_frame.pack(pady=10)
        tk.Button(button_frame, text="确定", command=apply_size).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="取消", command=size_window.destroy).pack(side=tk.LEFT, padx=5)

    def apply_font_to_selection(self):
        """将当前字体应用到选中内容"""
        try:
            start, end = self.text_area.index(tk.SEL_FIRST), self.text_area.index(tk.SEL_LAST)
            # 为选中内容创建自定义字体标签
            tag_name = f"custom_font_{self.current_font_family}_{self.current_font_size}_{int(time.time()*1000)}"
            # 配置字体标签
            custom_font = font.Font(
                family=self.current_font_family,
                size=self.current_font_size
            )
            self.text_area.tag_configure(tag_name, font=custom_font)
            self.text_area.tag_add(tag_name, start, end)
            # 同时更新粗体、斜体等标签的字体配置（仅对选中内容）
            self.update_format_tags_in_selection(start, end)
        except tk.TclError:
            pass  # 没有选中内容，不执行任何操作

    def update_format_tags_in_selection(self, start, end):
        """更新选中内容中的格式标签字体配置"""
        # 获取选中范围内的所有格式标签
        format_tags = ["bold", "italic", "underline", "strikethrough"]
        for tag_name in format_tags:
            # 检查选中范围内是否有该标签
            ranges = self.text_area.tag_ranges(tag_name)
            if not ranges:
                continue
            # 遍历所有标签范围，找出与选中范围重叠的部分
            i = 0
            while i < len(ranges):
                tag_start = ranges[i]
                tag_end = ranges[i + 1]
                # 检查范围是否与选中范围重叠
                if self.ranges_overlap(start, end, tag_start.string, tag_end.string):
                    # 计算实际重叠范围
                    overlap_start = self.max_index(start, tag_start.string)
                    overlap_end = self.min_index(end, tag_end.string)
                    # 为重叠部分创建新的标签
                    new_tag_name = f"{tag_name}_custom_{self.current_font_family}_{self.current_font_size}_{int(time.time()*1000)}"
                    # 配置新的字体
                    if tag_name == "bold":
                        custom_font = font.Font(
                            family=self.current_font_family,
                            size=self.current_font_size,
                            weight="bold"
                        )
                    elif tag_name == "italic":
                        custom_font = font.Font(
                            family=self.current_font_family,
                            size=self.current_font_size,
                            slant="italic"
                        )
                    else:
                        custom_font = font.Font(
                            family=self.current_font_family,
                            size=self.current_font_size
                        )
                    # 配置标签
                    if tag_name == "underline":
                        self.text_area.tag_configure(new_tag_name, font=custom_font, underline=True)
                    elif tag_name == "strikethrough":
                        self.text_area.tag_configure(new_tag_name, font=custom_font, overstrike=True)
                    else:
                        self.text_area.tag_configure(new_tag_name, font=custom_font)
                    # 应用标签到重叠部分
                    self.text_area.tag_add(new_tag_name, overlap_start, overlap_end)
                i += 2

    def ranges_overlap(self, start1, end1, start2, end2):
        """检查两个范围是否重叠"""
        def index_to_tuple(index):
            return tuple(map(int, index.split('.')))
        s1 = index_to_tuple(start1)
        e1 = index_to_tuple(end1)
        s2 = index_to_tuple(start2)
        e2 = index_to_tuple(end2)
        return s1 < e2 and s2 < e1

    def max_index(self, idx1, idx2):
        """返回两个索引中较大的一个"""
        def index_to_tuple(index):
            return tuple(map(int, index.split('.')))
        t1 = index_to_tuple(idx1)
        t2 = index_to_tuple(idx2)
        return idx1 if t1 >= t2 else idx2

    def min_index(self, idx1, idx2):
        """返回两个索引中较小的一个"""
        def index_to_tuple(index):
            return tuple(map(int, index.split('.')))
        t1 = index_to_tuple(idx1)
        t2 = index_to_tuple(idx2)
        return idx1 if t1 <= t2 else idx2

    def change_text_color(self):
        color = colorchooser.askcolor()[1]
        if color:
            try:
                start, end = self.text_area.index(tk.SEL_FIRST), self.text_area.index(tk.SEL_LAST)
                tag_name = f"color_{color.replace('#', '')}_{int(time.time()*1000)}"
                self.text_area.tag_configure(tag_name, foreground=color)
                self.text_area.tag_add(tag_name, start, end)
            except tk.TclError:
                pass

    # ==================== 自动保存 & 配置 & 回收站 & 备份 ====================
    def start_auto_save(self):
        def loop():
            while True:
                time.sleep(self.auto_save_interval)
                if self.unsaved_changes and self.current_file:
                    try:
                        with open(self.current_file, 'w', encoding='utf-8') as f:
                            f.write(self.text_area.get(1.0, tk.END).strip())
                        # 更新元数据修改时间
                        meta_path = self.current_file + '.meta.json'
                        if os.path.exists(meta_path):
                            try:
                                with open(meta_path, 'r', encoding='utf-8') as mf:
                                    meta = json.load(mf)
                                meta['modified_time'] = datetime.now().isoformat()
                                with open(meta_path, 'w', encoding='utf-8') as mf:
                                    json.dump(meta, mf, ensure_ascii=False, indent=2)
                            except Exception as e:
                                print(f"自动保存时更新元数据失败: {e}")
                        self.root.after(0, self.on_auto_save_success)
                    except Exception as e:
                        self.root.after(0, lambda: messagebox.showwarning("自动保存失败", str(e)))
        t = threading.Thread(target=loop, daemon=True)
        t.start()

    def on_auto_save_success(self):
        self.unsaved_changes = False
        self.update_title()
        # messagebox.showinfo("自动保存", "已自动保存")

    def start_trash_cleaner(self):
        """启动后台线程定期清理回收站"""
        def cleaner():
            while True:
                time.sleep(24 * 60 * 60) # 每天检查一次
                # time.sleep(30) # 测试时用 30 秒
                self.clean_trash()

        t = threading.Thread(target=cleaner, daemon=True)
        t.start()

    def clean_trash(self):
        """清理回收站中超过30天的项目"""
        trash_path = self.get_trash_path()
        if not os.path.exists(trash_path):
             return

        cutoff_time = time.time() - (30 * 24 * 60 * 60) # 30天前的时间戳

        try:
            for item in os.listdir(trash_path):
                item_path = os.path.join(trash_path, item)
                # 尝试从文件名解析时间戳 (假设格式为 timestamp_...)
                try:
                    # 提取时间戳部分 (前26位: YYYYMMDD_HHMMSS_microseconds)
                    timestamp_str = "_".join(item.split('_')[:3])
                    # 微秒需要补足6位
                    parts = timestamp_str.split('_')
                    if len(parts) == 3 and len(parts[2]) < 6:
                        parts[2] = parts[2].ljust(6, '0')
                    timestamp_str = "_".join(parts)
                    
                    item_time = datetime.strptime(timestamp_str, "%Y%m%d_%H%M%S_%f").timestamp()
                except (ValueError, IndexError) as e:
                    # 如果无法解析时间戳，则使用文件的修改时间
                    print(f"无法从文件名解析时间戳 {item}: {e}")
                    try:
                        item_time = os.path.getmtime(item_path)
                    except OSError:
                        item_time = time.time() # 如果获取失败，默认认为是现在，避免误删

                if item_time < cutoff_time:
                     try:
                         if os.path.isdir(item_path):
                             shutil.rmtree(item_path)
                         else:
                             os.remove(item_path)
                             meta_path = item_path + '.meta.json'
                             if os.path.exists(meta_path):
                                 os.remove(meta_path)
                         print(f"已清理回收站项目: {item}")
                     except Exception as e:
                         print(f"清理回收站项目 {item} 失败: {e}")
        except Exception as e:
             print(f"扫描回收站失败: {e}")

    def load_config(self):
        """加载配置，包括默认存储路径和备份设置"""
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    # 现有配置
                    self.recent_files = [fp for fp in data.get("recent_files", []) if os.path.exists(fp)]
                    self.clipboard_history = deque(data.get("clipboard_history", []), maxlen=5)
                    # --- 新增：加载存储和备份配置 ---
                    # 笔记根目录 (已在 load_notes_root 处理，这里加载默认值作为后备)
                    self.default_notes_root = data.get('default_notes_root') 
                    # 备份配置
                    self.backup_enabled = data.get('backup_enabled', False)
                    self.backup_directory = data.get('backup_directory', '')
                    self.backup_interval = data.get('backup_interval', 'daily')
                    self.backup_keep_count = data.get('backup_keep_count', 7)
                    # --- 新增结束 ---
            except Exception as e:
                print(f"加载配置失败：{e}")

    def save_config(self):
        """保存配置，包括默认存储路径和备份设置"""
        try:
            config_data = {
                # 现有配置
                "recent_files": self.recent_files,
                "clipboard_history": list(self.clipboard_history),
                # --- 新增：保存存储和备份配置 ---
                "notes_root": self.notes_root, # 保存当前笔记根目录
                "default_notes_root": self.default_notes_root, # 保存默认笔记根目录
                "backup_enabled": self.backup_enabled,
                "backup_directory": self.backup_directory,
                "backup_interval": self.backup_interval,
                "backup_keep_count": self.backup_keep_count,
                # --- 新增结束 ---
            }
            with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
                json.dump(config_data, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"保存配置失败：{e}")

    def exit_app(self):
        if not self.confirm_save_if_needed():
            return
        self.save_config()
        self.root.quit()

if __name__ == "__main__":
    root = tk.Tk()
    app = NoteApp(root)
    root.mainloop()